#!/bin/bash
#
# This hook is invoked by git commit, and can be bypassed with --no-verify
# option. It takes no parameter, and is invoked before obtaining the proposed
# commit log message and making a commit. Exiting with non-zero status from
# this script causes the git commit to abort.

if [[ -n "$(which make 2>&1)" ]]; then
	make git_hooks >/dev/null || exit
fi

if git rev-parse --verify HEAD &>/dev/null; then
	against=HEAD
else
	# Initial commit: diff against an empty tree object
	against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

# Check a hiera YAML file
#
function check_yaml_file () {
        local file=$1

        local yml=${workdir}/${file}
        mkdir -p $(dirname ${yml})
        git cat-file blob :0:${file} >${yml}
        trap "rm -f ${yml}" RETURN
        local output=$(ruby -r yaml -e "begin; foo = YAML.load(File.open('${yml}')); rescue ArgumentError => e ; print \"%s: %s\\n\" % ['${yml}', e] ; exit 1 ; end" )

        if [[ $? -ne 0 || -n "${output}" ]]; then
                echo '** Syntax check failed:' >&2
                echo "${output}" >&2
                return 1
        fi
}

# Check a Puppet manifest.
#
function check_puppet_manifest () {
	local file=$1

	local pp=${workdir}/${file}
	mkdir -p $(dirname ${pp})
	# --ignoreimport not working any more, comment out any import lines.
	git cat-file blob :0:${file} | sed 's/^import .*/#&/' >${pp}
	trap "rm -f ${pp}" RETURN

	local output=$(puppet parser validate ${pp} 2>&1)
	if [[ $? -ne 0 || -n "${output}" ]]; then
		echo '** Syntax check failed:' >&2
		echo "${output}" >&2
		return 1
	fi

	# Check manifests for exactly one class or define per file.
	case "${file}" in
		environments/*/modules/*/manifests/*|manifests/classes/*|modules/*/manifests/*)
			if [[ "$(egrep -c '^(class|define) ' ${file})" != "1" ]]; then
				echo "** Multiple class/defines (see Manifest Standards):" >&2
				egrep -n '^(class|define) ' ${file} >&2
				return 1
			fi
			;;
	esac

	# Check smoketests are present where required.
	if [[ ${file} =~ modules/[^/]+/manifests/ ]]; then
		if [[ -z "$(git show :0:${file/manifests/tests} 2>/dev/null)" ]]; then
			if [[ -z "$(git ls-files --cached -- $(dirname ${file/manifests/tests}) | egrep -e $(sed 's|\.pp$|\\.[^/]+\\.pp\$|' <<<${file/manifests/tests}))" ]]; then
				echo "** Corresponding smoketest is missing" >&2
				return 1
			fi
		fi
	fi

	case "${file}" in
		environments/manifests/*) ;;
		environments/modules/*/manifests/*) ;;
		manifests/*) ;;
		modules/*/manifests/*) ;;
		modules/*/tests/*) ;;
		*) return
	esac

	local wsfail=$({
		# grep -nPT would be nice. grep -P seems broken on gudeploy01.
		# Patterns checked for are (in this order):
		# - leading SPACE
		# - SPACE followed by TAB
		# - TAB followed by SPACE
		# - non-leading TAB (but allow around '#' comment leader)
		# - trailing whitespace
		pcregrep -n -e '^ ' -e ' \t' -e '\t ' -e '[^\t#]\t+(?!#)' -e '\s+$' ${pp}
	})
	if [[ $(wc -l <${pp}) > $(cat -s ${pp}|wc -l) ]]; then
		wsfail+="Excess blank line(s). 'cat -s' is your friend."
	fi
	if [[ -n "${wsfail}" ]]; then
		echo '** Bad whitespace (see Manifest Standards):' >&2
		echo "${wsfail}" >&2
		return 1
	fi

	# Re-export but this time expanding tabs for puppet-lint
	git cat-file blob :0:${file} | expand -t2 | sed 's/^import .*/#&/' >${pp}
	case "${file}" in
		environments/manifests/*|manifests/*)
			if [[ -n "$(which puppet-lint)" ]]; then
				puppet-lint --no-80chars-check \
					--no-inherits_across_namespaces-check --no-autoloader_layout-check \
					${pp} >&2 | uniq
			else
				echo "puppet-lint not found.  Please install it"
				echo "this will continue, but after a short pause to make you reflect on your ways"
				sleep 20
			fi
			;;
		environments/modules/*/manifests/*|modules/*/manifests/*)
			if [[ -n "$(which puppet-lint)" ]]; then
				puppet-lint --no-80chars-check ${pp} >&2 | uniq
			else
				echo "puppet-lint not found.  Please install it"
				echo "this will continue, but after a short pause to make you reflect on your ways"
				sleep 20
			fi
			;;
		*)
			return
			;;
	esac

	return 0
}

# Check a Puppet template.
#
function check_puppet_template () {
	local file=$1

	# Beware of 'Syntax OK' message on success.
	local output=$((git cat-file blob :0:${file}|erb -x -P -T -|ruby -c >/dev/null) 2>&1)
	if [[ $? -ne 0 || -n "${output}" ]]; then
		echo '** Syntax check failed:' >&2
		echo "${output}" >&2
		return 1
	fi

	return 0
}

# Check a ruby script.
#
function check_ruby_script () {
	local file=$1

	# Beware of 'Syntax OK' message on success.
	local output=$(git cat-file blob :0:${file}|ruby -c 2>&1 >/dev/null)
	if [[ $? -ne 0 || -n "${output}" ]]; then
		echo '** Syntax check failed:' >&2
		echo "${output}" >&2
		return 1
	fi

	return 0
}

# Check a shell script, assume it needs bash.
#
function check_shell_script () {
	local file=$1

	local output=$(git cat-file blob :0:${file}|bash -n 2>&1)
	if [[ $? -ne 0 || -n "${output}" ]]; then
		echo '** Syntax check failed:' >&2
		echo "${output}" >&2
		return 1
	fi

	return 0
}

readonly failed=$(mktemp ${TMPDIR:-/tmp}/git.puppet.pre-commit.XXXXXXXXXX)
readonly missing=$(mktemp ${TMPDIR:-/tmp}/git.puppet.pre-commit.XXXXXXXXXX)
readonly parse=$(mktemp ${TMPDIR:-/tmp}/git.puppet.pre-commit.XXXXXXXXXX)
readonly retest=$(mktemp ${TMPDIR:-/tmp}/git.puppet.pre-commit.XXXXXXXXXX)
readonly workdir=$(mktemp -d ${TMPDIR:-/tmp}/git.puppet.pre-commit.XXXXXXXXXX)
trap "rm -f ${failed} ${missing} ${parse} ${retest}; rm -rf ${workdir}" EXIT

smoketest=0
error=0
warning=0

for file in $(git diff-index --name-only --diff-filter=AM --cached ${against}); do
	output=""
	rc=0

	case "${file}" in
		manifests/*)
			case "${file}" in
				*.pp) output=$(check_puppet_manifest ${file} 2>&1); rc=$?;;
			esac
			;;

		modules/*)
			# No checking if Modulefile is present; assume it's a Forge module.
			modulefile=$(sed 's|^\(modules/[^/]*\)/.*$|\1/Modulefile|' <<<${file})
			[[ -n "$(git ls-files --cached -- ${modulefile})" ]] && continue

			case "${file}" in
				modules/*/lib/*)
					case "${file}" in
						*.rb) output=$(check_ruby_script ${file} 2>&1); rc=$?;;
						*.sh) output=$(check_shell_script ${file} 2>&1); rc=$?;;
					esac
					;;
				modules/*/manifests/*)
					case "${file}" in
						# FIXME: doing this so the whitespace check happens
						*.pp) output=$(check_puppet_manifest ${file} 2>&1); rc=$?;;
					esac
					smoketest=1
					;;
				modules/*/templates/*)
					smoketest=1
					;;
				modules/*/tests/*)
					smoketest=1
					;;
			esac
			;;

		templates/*)
			output=$(check_puppet_template ${file} 2>&1); rc=$?;;
		hieradata/*)
			output=$(check_yaml_file ${file} 2>&1); rc=$?;;
	esac

	if [[ ${rc} -ne 0 || -n "${output}" ]]; then
		echo "** ${file}" >&2
		echo "${output}" >&2
		if [[ ${file} != *.pp ]]; then
			error=1
		# puppet-lint warnings are acceptable.
		elif [[ -n "$(egrep -v '^WARNING: ' <<<${output})" ]]; then
			error=1
		else
			warning=1
		fi
	fi
done

if [ $error -ne 0 ]; then
	echo "** Please correct the above errors." >&2
	exit 1
fi

if [ $warning -ne 0 ]; then
	echo "** Please correct the above warnings (or wait 20s)." >&2
	sleep 20
fi

if [[ ${smoketest} -eq 1 ]]; then
	# Use the working tree if clean enough.
	# FIXME: git clean includes empty directories
	if [[ -z "$(git status -s -u manifests/ modules/)" ]]; then
		prefix="${PWD}"
	else
		# Export a copy of the staged tree.
		prefix="${PWD}/.pre-commit"
		echo -n 'exporting staged tree... '
		rm -rf ${prefix}
		git ls-files | egrep -v '^fileserver' \
			| git checkout-index --prefix=${prefix}/ --stdin
		echo 'done.'
	fi

	# Warn if changing a module in the production environment that also exists in
	# the development environment.
	envwarn=0
	for module in $(git diff-index --name-only --cached ${against} \
			| pcregrep -o '(?<=^modules/)[^/]+(?=/(?!tests/))' | uniq); do
		if [[ -d ${prefix}/environments/development/modules/${module} ]]; then
			echo "** WARNING: ${module} module exists in development environment" >&2
			envwarn=1
		fi
	done
	if [[ ${envwarn} -gt 0 ]]; then
		echo "** WARNING: THIS MIGHT NOT WORK AS YOU EXPECT" >&2
		sleep 5
	fi
	unset envwarn

	# FIXME: test environments/* too.

	files=$(git diff-index --cached --name-only --diff-filter=ACMRT ${against})
	cd ${prefix}/ || exit
	for file in ${files}; do
		[[ ${file} =~ ^modules/[^/]+/manifests/ ]] || continue
		[[ ${file} =~ /manifests/users-and-groups/ ]] && continue
		if [[ ${file} =~ \.pp$ ]]; then
			tests=$(sed 's|/manifests/|/tests/|;s|\.pp$|{,.*}.pp|' <<<${file})
			if [[ -z "$(eval ls ${tests} 2>/dev/null)" ]]; then
				echo ${file} >>${missing}
			else
				for test in $(eval ls ${tests} 2>/dev/null); do
					echo ${test} >>${retest}
				done
			fi
		fi
	done
	cd - >/dev/null
	unset files

	files=$(git diff-index --cached --name-only --diff-filter=ACDMRT ${against})
	cd ${prefix}/ || exit
	for file in ${files}; do
		[[ ${file} =~ ^modules/[^/]+/templates/ ]] || continue
		template=$(sed 's|^modules/||;s|/templates/||' <<<${file})
		manifests=$(pcregrep -lr "\\b\\Q${template}\\U\\b" modules/*/manifests)
		if [[ -n "${manifests}" ]]; then
			for manifest in ${manifests}; do
				if [[ ${manifest} =~ \.pp$ ]]; then
					tests=$(sed 's|/manifests/|/tests/|;s|\.pp$|{,.*}.pp|' <<<${manifest})
					if [[ -z "$(eval ls ${tests} 2>/dev/null)" ]]; then
						echo ${manifest} >>${missing}
					else
						for test in $(eval ls ${tests} 2>/dev/null); do
							echo ${test} >>${retest}
						done
					fi
				fi
			done
		fi
	done
	cd - >/dev/null
	unset files

	files1=$(git diff-index --cached --name-only --diff-filter=D ${against})
	files2=$(git diff-index --cached --name-only --diff-filter=ACMRT ${against})
	cd ${prefix}/ || exit
	for file in ${files1}; do
		[[ ${file} =~ ^modules/[^/]+/tests/ ]] || continue
		if [[ ${file} =~ \.facts$ ]]; then
			test=${file/%facts/pp}
			[[ -e ${prefix}/${test} ]] && echo ${test} >>${retest}
		fi
		if [[ ${file} =~ \.pp$ ]]; then
			manifest=$(sed 's|/tests/|/manifests/|;s|\..*\.pp$|.pp|' <<<${file})
			[[ -f ${prefix}/${manifest} ]] && echo ${manifest} >>${missing}
		fi
	done
	for file in ${files2}; do
		[[ ${file} =~ ^modules/[^/]+/tests/ ]] || continue
		[[ ${file} =~ \.facts$ ]] && file=${file/%facts/pp}
		[[ ${file} =~ \.pp$ ]] && echo ${file} >>${retest}
	done
	cd - >/dev/null
	unset files1 files2

	if [[ -s ${missing} ]]; then
		nmissing=$(sort -u ${missing}|wc -l|tr -d [[:space:]])
		if [[ ${nmissing} -gt 0 ]]; then
			[[ ${nmissing} -gt 1 ]] && s='s'
			echo "** ${nmissing} missing test${s}: $(sed 's|/manifests/|/|;s|\.pp$||' ${missing})" >&2
			exit 1
		fi
	fi

	if [[ -s ${retest} ]]; then
		ntest=$(echo $(sort -u ${retest}|wc -l)|tr -d [[:space:]])
		[[ ${ntest} -gt 1 ]] && s='s'
		echo "running ${ntest} test${s}"
		unset s

		cd ${prefix}/ || exit

		mkdir -m 0700 .modules
		find $(ls -d modules/*) -type d -exec mkdir -m 0700 .{} \;
		find $(ls -d modules/*/{files,lib,templates}) -type f -exec ln {} .{} \;
		find $(ls -d modules/*/{files,lib,templates}) -type l -exec cp -p {} .{} \;
		mkdir config
		cat <<EOF > config/hiera.yaml
---
:hierarchy:
 - common
:backends:
 - yaml
:yaml:
 :datadir:
EOF

		for file in $(find modules/*/manifests -type f -name '*.pp'); do
			define="$(pcregrep -Mo '^define\s+\S+\s*\([^)]*\)\s*{' $file)"
			[[ -n "${define}" ]] && echo "${define}}" >.${file}
		done

		# Need to run smoketests as root for puppet-deploy.
		#if [[ "$(id -u)" != 0 ]]; then
		#	sudo='sudo -E --'
		#fi

		# Some components need to know that this is a test. Currently this is
		# only the instancetag provider. Use sparingly!
		export FACTER_smoketesting=true
		export PUPPET_SMOKE_TEST=1

		# Emulate root_group setting in site.pp.
		if [ "$(uname)" = 'Darwin' ]; then
			export FACTER_root_group=wheel
		else
			export FACTER_root_group=root
		fi

		for test in $(sort -u ${retest}); do
			manifest=$(sed 's|/tests/|/manifests/|;s|\..*\.pp$|.pp|' <<<${test})
			[[ -e .${manifest} ]] && mv .${manifest} .${manifest}.stub
			ln ${manifest} .${manifest}
			# '--templatedir' is undesirable, remove it as soon as practical to do so.
			output=$(
				env $(perl -ne '/^(\w+=.*?)\s*(?:#.*)?$/ && print "FACTER_$1\n"' ${test/%\.pp/.facts} 2>/dev/null) \
					${sudo} \
						puppet apply --noop --confdir config --manifestdir manifests --modulepath .modules --puppetdlockfile .modules/puppetdlock --templatedir templates ${test} 2>&1 \
			)
			if [[ $? -ne 0 ]]; then
				sed "s|/\.pre-commit/|/|g;s|/\.modules/|/modules/|g" <<<"${output}" >&2
				echo "FAILED: ${test}" >&2
				echo ${test} >>${failed}
			fi
			rm .${manifest}
			[[ -e .${manifest}.stub ]] && mv .${manifest}.stub .${manifest}
		done

		unset FACTER_root_group FACTER_smoketesting PUPPET_SMOKE_TEST

		cd - >/dev/null

		nfailed=$(wc -l <${failed} | tr -d [[:space:]])
		if [[ ${nfailed} -gt 0 ]]; then
			[[ ${nfailed} -gt 1 ]] && s='s'
			echo "** ${nfailed} failed test${s}: $(sed 's|/tests/|/|;s|\.pp$||' ${failed})" 2>/dev/null
			exit 1
		fi
	fi
fi

exit 0
